﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MWS.Processing.Domain;
using MWS.Processing.Interfaces;

namespace MWS.Processing.Processors
{
    /// <summary>
    ///   Use this class to implement a processing system. The processing system will continuously add new requests to a processing
    ///   requestQueue, while at the same time it will continuously process the requests from the requestQueue. The rate of processing can be throttled.
    /// </summary>
    /// <typeparam name = "TRequest">The type of request that will be processed.</typeparam>
    public abstract class Processor<TRequest>
        : IProcessor
        where TRequest : class
    {
        /// <summary>
        ///   The default constuctor. The only required property is the operation. All other properties have default values.
        /// </summary>
        protected Processor()
        {
            // Set the initial performance variables.
            RequestQueueSize = 1;
            GetRequestsRate = 1000;
            ProcessRequestsRate = 1000;
            ProcessRequestsMaxParallel = 1;


            // Wire up the state change event handlers.
            Initializing += OnInitializing;
            Running += OnRunning;
            Stopping += OnStopping;
            Stopped += OnStopped;
        }

        /// <summary>
        ///   The max number of requests to be stored in the requestQueue.
        /// </summary>
        public int RequestQueueSize { get; set; }

        /// <summary>
        ///   The current number of requests needed to refill the requestQueue.
        /// </summary>
        public int RequestBatchSize
        {
            get { return RequestQueueSize - requestQueue.Count - processingList.Count; }
        }

        /// <summary>
        ///   The rate (in milliseconds) that the request loop will sleep between checking if new requests
        ///   are needed to fill the requestQueue.
        /// </summary>
        public int GetRequestsRate { get; set; }

        /// <summary>
        ///   The rate (in milliseconds) that the process loop will sleep between checkig if the requestQueue
        ///   has requests waiting to be processed.
        /// </summary>
        public int ProcessRequestsRate { get; set; }

        /// <summary>
        ///   The max number of requests that will be taken from the requestQueue each time the processing loop
        ///   iterates and finds requests to process in the requestQueue.
        /// </summary>
        public int ProcessRequestsMaxParallel { get; set; }

        /// <summary>
        ///   Displays the status this processor is currently in. A status will typically start out as 
        ///   Stopped, then moved to Initializing, then to Running, then to Stopping, then Stopped.
        /// </summary>
        public RunningStatus Status { get; set; }

        /// <summary>
        ///   This will start the request loop which fills the requestQueue with requests, as well
        ///   as the processing loop which processes requests from the requestQueue.
        /// </summary>
        public void Start()
        {
            // Fire initialize event.
            Initializing(this, null);
            // Start up the loop that will constantly add new requests to the requestQueue.
            ThreadPool.QueueUserWorkItem(GetRequestsLoop);
            // Start up the loop that will constantly process requests from the requestQueue.
            ThreadPool.QueueUserWorkItem(ProcessRequestsLoop);
            // Fire running event.
            Running(this, null);
        }

        /// <summary>
        ///   This will fire a stopping and stopped event. The stopped event will cause the
        ///   request and processing loops to stop. However any processing threads running
        ///   in the background will continue to run untill the have completed processing.
        /// </summary>
        public void Stop()
        {
            // Fire stopping event.
            Stopping(this, null);

            // Wait until the processing list is done processing requests.
            while (processingList.Count > 0)
                Thread.Sleep(10);

            // Fire stopped event.
            Stopped(this, null);
        }

        /// <summary>
        ///   This runs on it's own thread, constantly looking at the requestQueue to see if it needs to be
        ///   refilled with more requests. The amount of requests it refills the requestQueue with is determined
        ///   by the request batch size (requestQueue max size - requestQueue.count).
        /// </summary>
        /// <param name = "obj">Not used, just here because the WaitCallback delegate requires it.</param>
        private void GetRequestsLoop(object obj)
        {
            // Ensure we have a running status.
            while (Status == RunningStatus.Running)
            {
                var iterationStart = DateTime.Now;

                // Ensure the requestQueue is not currently full.
                if (RequestBatchSize > 0)
                {
                    // Get requests, only taking the number needed to fill the requestQueue.
                    // The Take operation is called, just incase the implementor of this method
                    // didn't acutally limit the number of requests to the request batch size.
                    var requests = GetRequests(RequestBatchSize).Take(RequestBatchSize);
                    // Add requests to the requestQueue.
                    lock (requestQueue)
                        foreach (var request in requests)
                            requestQueue.Enqueue(request);
                }

                var sleep = GetRequestsRate -
                        Convert.ToInt32(DateTime.Now.Subtract(iterationStart).TotalMilliseconds);

                if (sleep > 0)
                    Thread.Sleep(sleep);
            }
        }

        /// <summary>
        ///   This runs on it's own thread, constantly looking for requests in the requestQueue that need to be
        ///   processed. It will run parallel requests to the level of parallelism defined. It will sleep
        ///   for a configurable amount of time between requestQueue checks.
        /// </summary>
        /// <param name = "obj">Not used, just here because the WaitCallback delegate requires it.</param>
        private void ProcessRequestsLoop(object obj)
        {
            // Ensure we have a running status.
            while (Status == RunningStatus.Running)
            {
                var iterationStart = DateTime.Now;

                for (var i = 0; i < ProcessRequestsMaxParallel; i++)
                {
                    // Ensure the requestQueue has at least one request ready for processing.
                    if (requestQueue.Count > 0)
                    {
                        // Remove the request from the requestQueue.
                        TRequest request;
                        lock (requestQueue)
                            request = requestQueue.Dequeue();
                        lock (processingList)
                            processingList.Add(request);
                    }
                }

                // Process the requests in parallel. This will block untill the last request has finished.
                Parallel.ForEach(processingList,
                                 new ParallelOptions
                                     {
                                         MaxDegreeOfParallelism = ProcessRequestsMaxParallel
                                     },
                                 ProcessRequest);
                processingList.Clear();

                var sleep = ProcessRequestsRate -
                            Convert.ToInt32(DateTime.Now.Subtract(iterationStart).TotalMilliseconds);

                if (sleep > 0)
                    Thread.Sleep(sleep);
            }
        }


        /// <summary>
        ///   This will fire the pre process, process, and post process events.
        /// </summary>
        /// <param name = "request">The request to process.</param>
        private void ProcessRequest(TRequest request)
        {
            // If there is a pre process handler registered, fire the pre process event.
            if (PreProcess != null)
                PreProcess(this, new RequestEventArgs {Request = request});

            // If there is a process handler registered, fire the process event.);
            if (Process != null)
                Process(this, new RequestEventArgs {Request = request});

            // If ther eis a post process handler registered, fire the post process event.)
            if (PostProcess != null)
                // Send the post process event with the request and response.
                PostProcess(this, new RequestEventArgs {Request = request});
        }

        /// <summary>
        ///   This is where you need to implement the logic to return a list of requests. The request loop will call this 
        ///   method periodically for new requests to add to the requestQueue. You should use the max request parameter to determine
        ///   how many new requests you should return.
        /// </summary>
        /// <param name = "maxRequests">Use this parameter to limit the number of items you return in the list of requests.</param>
        public abstract IList<TRequest> GetRequests(int maxRequests);

        /// <summary>
        ///   This event is fired just before the the operation is about to process a request. This allows you to 
        ///   have a look at the request just before it's processed.
        /// </summary>
        public event RequestHandler PreProcess;

        /// <summary>
        ///   This event is fire when the operation is processing the request.
        /// </summary>
        public event RequestHandler Process;

        /// <summary>
        ///   This event is fired just after the process event completes. This allows you look at the request and response just
        ///   after the processing happened. You might want to use this response to log that it has been processed, etc..
        /// </summary>
        public event RequestHandler PostProcess;


        /// <summary>
        ///   This is fired when the processing system is starting up, before it actually starts.
        /// </summary>
        public event StatusChangeHandler Initializing;

        /// <summary>
        ///   This is fired when the processing system has started running.
        /// </summary>
        public event StatusChangeHandler Running;

        /// <summary>
        ///   This is fired when the processing system is stopping.
        /// </summary>
        public event StatusChangeHandler Stopping;

        /// <summary>
        ///   This is fired when the processing system has stopped running.
        /// </summary>
        public event StatusChangeHandler Stopped;


        /// <summary>
        ///   Delegate for a handler of an request event.
        /// </summary>
        /// <param name = "sender">The sender of the event.</param>
        /// <param name = "args">The arguments of the event, the request.</param>
        public delegate void RequestHandler(object sender, RequestEventArgs args);

        /// <summary>
        ///   Delegate for a handler of a status change event.
        /// </summary>
        /// <param name = "sender">Sender of the status change event.</param>
        /// <param name = "args">The arguments of the status change event.</param>
        public delegate void StatusChangeHandler(object sender, StatusChangedEventArgs args);

        /// <summary>
        ///   Event arguments about a request.
        /// </summary>
        public class RequestEventArgs : EventArgs
        {
            /// <summary>
            ///   The request being processed.
            /// </summary>
            public TRequest Request { get; set; }
        }

        /// <summary>
        ///   Event arguments about a status change.
        /// </summary>
        public class StatusChangedEventArgs : EventArgs
        {
        }

        /// <summary>
        ///   This is the requestQueue that holds the requests waiting to be processed. The request loop 
        ///   fills this requestQueue up, and the process loop removes requests from the requestQueue.
        /// </summary>
        private readonly Queue<TRequest> requestQueue = new Queue<TRequest>();

        /// <summary>
        ///   A list of requests that are being processed. As requests move out of the requestQueue, they
        ///   are put into the processingQueue. When they are finished processing, they are removed
        ///   from the processingQueue.
        /// </summary>
        private readonly List<TRequest> processingList = new List<TRequest>();

        /// <summary>
        ///   Handler for the stopped event.
        /// </summary>
        /// <param name = "sender">Sender of the event.</param>
        /// <param name = "args">Event arguments.</param>
        private void OnStopped(object sender, StatusChangedEventArgs args)
        {
            // Set the status to stopped.
            Status = RunningStatus.Stopped;
        }

        /// <summary>
        ///   Handler for the stopping event.
        /// </summary>
        /// <param name = "sender">Sender of the event.</param>
        /// <param name = "args">Event arguments.</param>
        protected void OnStopping(object sender, StatusChangedEventArgs args)
        {
            // Set the status to stopping.
            Status = RunningStatus.Stopping;
        }

        /// <summary>
        ///   Handler for the running event.
        /// </summary>
        /// <param name = "sender">Sender of the event.</param>
        /// <param name = "args">Event arguments.</param>
        private void OnRunning(object sender, StatusChangedEventArgs args)
        {
            // Set the stust to running.
            Status = RunningStatus.Running;
        }

        /// <summary>
        ///   Handler for the initializing event.
        /// </summary>
        /// <param name = "sender">Sender of the event.</param>
        /// <param name = "args">Event arguments.</param>
        private void OnInitializing(object sender, StatusChangedEventArgs args)
        {
            Status = RunningStatus.Initializing;
        }
    }
}